﻿/**
 * @license Rangy Text Inputs, a cross-browser textarea and text input library

 * Part of Rangy, a cross-browser JavaScript range and selection library
 * http://code.google.com/p/rangy/
 *
 * This file is standalone.
 *
 * Copyright %%build:year%%, Tim Down
 * Licensed under the MIT license.
 * Version: %%build:version%%
 * Build date: %%build:date%%
 */
var rangyInputs;

(function () {
	var UNDEF = "undefined";
	var getSelection, setSelection, deleteSelectedText, deleteText, insertText;
	var replaceSelectedText, surroundSelectedText, extractSelectedText, collapseSelection;
	var initialized = false;

	// Trio of isHost* functions taken from Peter Michaux's article:
	// http://peter.michaux.ca/articles/feature-detection-state-of-the-art-browser-scripting
	function isHostMethod(object, property) {
		var t = typeof object[property];
		return t === "function" || (!!(t == "object" && object[property])) || t == "unknown";
	}

	function isHostProperty(object, property) {
		return typeof (object[property]) != UNDEF;
	}

	function isHostObject(object, property) {
		return !!(typeof (object[property]) == "object" && object[property]);
	}

	function fail(reason) {
		if (window.console && window.console.log) {
			window.console.log("Rangy Text Inputs not supported in your browser. Reason: " + reason);
		}
	}

	function adjustOffsets(el, start, end) {
		if (start < 0) {
			start += el.value.length;
		}
		if (typeof end == UNDEF) {
			end = start;
		}
		if (end < 0) {
			end += el.value.length;
		}
		return { start: start, end: end };
	}

	function makeSelection(el, start, end) {
		return {
			start: start,
			end: end,
			length: end - start,
			text: el.value.slice(start, end)
		};
	}

	function getBody() {
		return isHostObject(document, "body") ? document.body : document.getElementsByTagName("body")[0];
	}

	function init() {
		if (initialized) {
			return;
		}

		var testTextArea = document.createElement("textarea");
		getBody().appendChild(testTextArea);

		if (isHostProperty(testTextArea, "selectionStart") && isHostProperty(testTextArea, "selectionEnd")) {
			getSelection = function (el) {
				var start = el.selectionStart, end = el.selectionEnd;
				return makeSelection(el, start, end);
			};

			setSelection = function (el, startOffset, endOffset) {
				var offsets = adjustOffsets(el, startOffset, endOffset);
				el.selectionStart = offsets.start;
				el.selectionEnd = offsets.end;
			};

			collapseSelection = function (el, toStart) {
				if (toStart) {
					el.selectionEnd = el.selectionStart;
				} else {
					el.selectionStart = el.selectionEnd;
				}
			};
		} else if (isHostMethod(testTextArea, "createTextRange") && isHostObject(document, "selection") &&
							 isHostMethod(document.selection, "createRange")) {

			getSelection = function (el) {
				var start = 0, end = 0, normalizedValue, textInputRange, len, endRange;
				var range = document.selection.createRange();

				if (range && range.parentElement() == el) {
					len = el.value.length;

					normalizedValue = el.value.replace(/\r\n/g, "\n");
					textInputRange = el.createTextRange();
					textInputRange.moveToBookmark(range.getBookmark());
					endRange = el.createTextRange();
					endRange.collapse(false);
					if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
						start = end = len;
					} else {
						start = -textInputRange.moveStart("character", -len);
						start += normalizedValue.slice(0, start).split("\n").length - 1;
						if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
							end = len;
						} else {
							end = -textInputRange.moveEnd("character", -len);
							end += normalizedValue.slice(0, end).split("\n").length - 1;
						}
					}
				}

				return makeSelection(el, start, end);
			};

			// Moving across a line break only counts as moving one character in a TextRange, whereas a line break in
			// the textarea value is two characters. This function corrects for that by converting a text offset into a
			// range character offset by subtracting one character for every line break in the textarea prior to the
			// offset
			var offsetToRangeCharacterMove = function (el, offset) {
				return offset - (el.value.slice(0, offset).split("\r\n").length - 1);
			};

			setSelection = function (el, startOffset, endOffset) {
				var offsets = adjustOffsets(el, startOffset, endOffset);
				var range = el.createTextRange();
				var startCharMove = offsetToRangeCharacterMove(el, offsets.start);
				range.collapse(true);
				if (offsets.start == offsets.end) {
					range.move("character", startCharMove);
				} else {
					range.moveEnd("character", offsetToRangeCharacterMove(el, offsets.end));
					range.moveStart("character", startCharMove);
				}
				range.select();
			};

			collapseSelection = function (el, toStart) {
				var range = document.selection.createRange();
				range.collapse(toStart);
				range.select();
			};
		} else {
			getBody().removeChild(testTextArea);
			fail("No means of finding text input caret position");
			return;
		}

		// Clean up
		getBody().removeChild(testTextArea);

		deleteText = function (el, start, end, moveSelection) {
			var val;
			if (start != end) {
				val = el.value;
				el.value = val.slice(0, start) + val.slice(end);
			}
			if (moveSelection) {
				setSelection(el, start, start);
			}
		};

		deleteSelectedText = function (el) {
			var sel = getSelection(el);
			deleteText(el, sel.start, sel.end, true);
		};

		extractSelectedText = function (el) {
			var sel = getSelection(el), val;
			if (sel.start != sel.end) {
				val = el.value;
				el.value = val.slice(0, sel.start) + val.slice(sel.end);
			}
			setSelection(el, sel.start, sel.start);
			return sel.text;
		};

		insertText = function (el, text, index, moveSelection) {
			var val = el.value, caretIndex;
			el.value = val.slice(0, index) + text + val.slice(index);
			if (moveSelection) {
				caretIndex = index + text.length;
				setSelection(el, caretIndex, caretIndex);
			}
		};

		replaceSelectedText = function (el, text) {
			var sel = getSelection(el), val = el.value;
			el.value = val.slice(0, sel.start) + text + val.slice(sel.end);
			var caretIndex = sel.start + text.length;
			setSelection(el, caretIndex, caretIndex);
		};

		surroundSelectedText = function (el, before, after) {
			var sel = getSelection(el), val = el.value;
			el.value = val.slice(0, sel.start) + before + sel.text + after + val.slice(sel.end);
			var startIndex = sel.start + before.length;
			var endIndex = startIndex + sel.length;
			setSelection(el, startIndex, endIndex);
		};

		rangyInputs = {
			init: init,
			getSelection: getSelection,
			setSelection: setSelection,
			collapseSelection: collapseSelection,
			deleteSelectedText: deleteSelectedText,
			deleteText: deleteText,
			extractSelectedText: extractSelectedText,
			insertText: insertText,
			replaceSelectedText: replaceSelectedText,
			surroundSelectedText: surroundSelectedText,
			addInitListener: function (func) {
				func();
			}
		};

		initialized = true;

		for (var i = 0, len = initListeners.length; i < len; ++i) {
			initListeners[i]();
		}
	}

	// Wait for document to load before creating API

	var docReady = false;

	var loadHandler = function (e) {
		if (!docReady) {
			docReady = true;
			if (!initialized) {
				init();
			}
		}
	};

	// Test whether we have window and document objects that we will need
	if (typeof window == UNDEF) {
		fail("No window found");
		return;
	}
	if (typeof document == UNDEF) {
		fail("No document found");
		return;
	}

	if (isHostMethod(document, "addEventListener")) {
		document.addEventListener("DOMContentLoaded", loadHandler, false);
	}

	// Add a fallback in case the DOMContentLoaded event isn't supported
	if (isHostMethod(window, "addEventListener")) {
		window.addEventListener("load", loadHandler, false);
	} else if (isHostMethod(window, "attachEvent")) {
		window.attachEvent("onload", loadHandler);
	} else {
		fail("Window does not have required addEventListener or attachEvent method");
	}

	var initListeners = [];

	function addInitListener(func) {
		initListeners.push(func);
	}

	rangyInputs = {
		init: init,
		addInitListener: addInitListener
	};
})();


(function () {
	if (!window.rangyInputs) {
		if (window.console && window.console.log) {
			window.console.log("The RangyInputs indentation module depends on the base RangyInputs script");
		}
	}

	window.rangyInputs.addInitListener(function () {
		var api = window.rangyInputs;

		// Test to see what character(s) is/are used for new lines in textareas in the current browser
		var testTextArea = document.createElement("textarea");
		document.body.appendChild(testTextArea);
		testTextArea.value = "1\r\n2";
		var newLine = testTextArea.value.match(/\r\n|\n|\r/)[0], newLineLen = newLine.length;
		document.body.removeChild(testTextArea);

		function prepareToIndentOrOutdent(el, sel) {
			var val = el.value, firstNewLineIndex = val.indexOf(newLine);
			if (firstNewLineIndex > -1) {
				var valBeforeSel = val.slice(0, sel.start);
				var valToMutateStart, valToMutateEnd, valBefore, valAfter;

				if (firstNewLineIndex >= sel.start) {
					valToMutateStart = 0;
					valBefore = "";
				} else {
					var lastNewLineBeforeSelIndex = valBeforeSel.lastIndexOf(newLine);
					valToMutateStart = lastNewLineBeforeSelIndex + newLineLen;
					valBefore = val.slice(0, valToMutateStart);
				}

				var firstNewLineAfterSelIndex = val.indexOf(newLine, sel.end - newLineLen);
				if (firstNewLineAfterSelIndex > -1) {
					valToMutateEnd = firstNewLineAfterSelIndex;
					valAfter = val.slice(firstNewLineAfterSelIndex + newLineLen);
				} else {
					valToMutateEnd = val.length;
					valAfter = "";
				}

				return {
					val: val,
					valToMutateStart: valToMutateStart,
					valToMutateEnd: valToMutateEnd,
					valToMutate: val.slice(valToMutateStart, valToMutateEnd),
					valBefore: valBefore,
					valAfter: valAfter
				}
			}
			return null;
		}

		function indentSelection(el, sel, indentation) {
			var info = prepareToIndentOrOutdent(el, sel);
			if (info) {
				var lines = info.valToMutate.split(newLine);
				var indented = indentation + lines.join(newLine + indentation) + newLine;
				var newSelStart = info.valToMutateStart == sel.start ? sel.start : sel.start + indentation.length;
				var newSelEnd = sel.end + lines.length * indentation.length;

				el.value = info.valBefore + indented + info.valAfter;
				api.setSelection(el, newSelStart, newSelEnd);
			}
		}

		function outdentSelection(el, sel, indentation) {
			var info = prepareToIndentOrOutdent(el, sel);
			if (info) {
				var lines = info.valToMutate.split(newLine);
				var outdentedLines = [], indentationLength = indentation.length, charsRemovedCount = 0, firstLineCharsRemovedCount;
				for (var i = 0, len = lines.length, lineCharsToRemoveCount; i < len; ++i) {
					lineCharsToRemoveCount = Math.min(lines[i].match(/^\s*/)[0].length, indentationLength);
					if (i == 0) {
						firstLineCharsRemovedCount = lineCharsToRemoveCount;
					}
					charsRemovedCount += lineCharsToRemoveCount;
					outdentedLines[i] = lines[i].slice(lineCharsToRemoveCount) + newLine;
				}
				var outdented = outdentedLines.join("");
				var newSelStart = Math.max(info.valToMutateStart == sel.start ? sel.start : sel.start - firstLineCharsRemovedCount, info.valToMutateStart);
				var newSelEnd = Math.max(sel.end - charsRemovedCount, info.valToMutateStart);

				el.value = info.valBefore + outdented + info.valAfter;
				api.setSelection(el, newSelStart, newSelEnd);
			}
		}

		function enableTabIndentation(el, indentation) {
			el.onkeydown = function (e) {
				e = e || window.event;
				if (e.keyCode == 9) {
					var sel = api.getSelection(el);
					if (e.shiftKey) {
						outdentSelection(el, sel, indentation);
					} else if (sel.length) {
						indentSelection(el, sel, indentation);
					} else {
						api.insertText(el, "\t", sel.end, true);
					}
					return false;
				}
			};

			// For Opera, which only allows suppression of keypress events, not keydown
			el.onkeypress = function (e) {
				e = e || window.event;
				if (e.which == 9) {
					return false;
				}
			};
		}

		api.enableTabIndentation = function (el, indentation) {
			if (el.nodeType == 1) {
				var nodeName = el.nodeName.toLowerCase();
				if (nodeName == "textarea" || (nodeName == "input" && el.type == "text")) {
					enableTabIndentation(el, indentation || "  ");
				}
			}
		};
	});
})();